import { SceneData } from '../../types/SceneTypes';
import { TimelineState } from '../../types/TimelineTypes';
import { SceneManager } from './SceneManager';
import { Camera } from './Camera';
import { CanvasManager } from './CanvasManager';
import { SceneElement } from './SceneElement';
import { CharacterElement } from './CharacterElement';
import { BackgroundElement } from './BackgroundElement';
import { UIElement } from './UIElement';
import { Logger } from '../../utils/Logger';

/**
 * AnimationRenderer Class
 *
 * The main class responsible for orchestrating the rendering process.
 * It manages scene elements, camera, and rendering context.
 */
export class AnimationRenderer {
  private sceneManager: SceneManager;
  private camera: Camera;
  private canvasManager: CanvasManager;
  private width: number;
  private height: number;
  private frameRate: number;
  private _currentProjectId: string | null = null; // Private backing field

  /**
   * Gets the current project ID
   * @returns The current project ID or null if not set
   */
  public get currentProjectId(): string | null {
    return this._currentProjectId;
  }

  /**
   * Sets the current project ID
   * @param id - The project ID to set
   */
  public set currentProjectId(id: string | null) {
    this._currentProjectId = id;
  }

  /**
   * Creates an instance of AnimationRenderer.
   * @param width - Width of the rendering area
   * @param height - Height of the rendering area
   */
  constructor(width: number, height: number, frameRate: number = 30) {
    this.width = width;
    this.height = height;
    this.frameRate = frameRate;
    this.sceneManager = new SceneManager();
    this.camera = new Camera();
    this.canvasManager = new CanvasManager(width, height);
  }

  /**
   * Renders a complete scene
   * @param sceneData - Scene data to render
   * @returns any - The canvas with the rendered scene
   */
  public async renderScene(sceneData: SceneData): Promise<any> {
    await this.setupScene(sceneData);
    this.renderElements();
    return this.canvasManager.getCanvas();
  }

  /**
   * Renders a single frame based on timeline state
   * @param timelineState - Current timeline state
   * @returns any - Buffer of the rendered frame
   */
  public renderFrame(timelineState: TimelineState): any {
    const logger = Logger.getInstance();
    logger.log(`Rendering frame with timeline state: ${JSON.stringify(timelineState)}`, 'AnimationRenderer');
    console.log('[AnimationRenderer] Rendering frame with timeline state:', timelineState);
    
    // Update elements based on timeline state
    const elements = this.sceneManager.getAllElements();
    logger.log(`Updating ${elements.length} elements based on timeline state`, 'AnimationRenderer');
    console.log(`[AnimationRenderer] Updating ${elements.length} elements based on timeline state`);
    
    for (const element of elements) {
      logger.log(`Updating element: ${element.getId()} (${element.constructor.name})`, 'AnimationRenderer');
      console.log(`[AnimationRenderer] Updating element: ${element.getId()} (${element.constructor.name})`);
      element.update(timelineState);
      logger.log(`Finished updating element: ${element.getId()}`, 'AnimationRenderer');
      console.log(`[AnimationRenderer] Finished updating element: ${element.getId()}`);
    }
    
    // Clear and render
    logger.log('Clearing canvas', 'AnimationRenderer');
    console.log('[AnimationRenderer] Clearing canvas');
    this.canvasManager.clear();
    
    logger.log('Rendering elements', 'AnimationRenderer');
    console.log('[AnimationRenderer] Rendering elements');
    this.renderElements();

    // Return frame as buffer
    logger.log('Converting canvas to buffer', 'AnimationRenderer');
    console.log('[AnimationRenderer] Converting canvas to buffer');
    const buffer = this.canvasManager.toBuffer();
    logger.log('Frame rendering completed', 'AnimationRenderer');
    console.log('[AnimationRenderer] Frame rendering completed');
    return buffer;
  }

  /**
   * Exports frames as video file
   * @param timelineStates - Array of timeline states for each frame
   * @param outputPath - Path to save the output video
   * @param format - Video format (mp4, webm, etc.)
   * @param signal - Optional AbortSignal to cancel the export operation
   * @param projectId - Optional project ID for asset loading
   * @returns Promise<void>
   */
  public async exportVideo(
    timelineStates: TimelineState[],
    outputPath: string,
    format: string = 'mp4',
    signal?: AbortSignal,
    projectId?: string
  ): Promise<void> {
    const logger = Logger.getInstance();
    
    try {
      // Set current project ID if provided
      if (projectId) {
        this.currentProjectId = projectId;
        logger.log(`Setting current project ID: ${projectId}`, 'AnimationRenderer');
      }
      
      logger.log(`Starting export of ${timelineStates.length} frames for project: ${projectId || 'unknown'}`, 'AnimationRenderer');
      
      // Dynamically import required modules
      logger.log(' -> 1. Importing required modules... fs ', 'AnimationRenderer');
      const fs = await import('fs');
      logger.log(' -> 2. Importing required modules... path ', 'AnimationRenderer');
      const path = await import('path');
      logger.log(' -> 3. Importing required modules... child_process ', 'AnimationRenderer');
      const { spawn } = await import('child_process');
      logger.log( `ffmpeg : ${process.env.FFMPEG_PATH}`, 'AnimationRenderer');
      
      // Create temporary directory for frames using project ID for isolation
      const tempDir = path.join(process.cwd(), 'temp', `frames_${projectId || 'default'}`);
      logger.log(`Creating temporary directory: ${tempDir}`, 'AnimationRenderer');
      if (!fs.existsSync(tempDir)) {
        fs.mkdirSync(tempDir, { recursive: true });
      }

      // Render each frame and save as PNG
      const framePaths: string[] = [];
      logger.log(`Rendering ${timelineStates.length} frames`, 'AnimationRenderer');
      
      for (let i = 0; i < timelineStates.length; i++) {
        // Check if operation has been aborted before rendering each frame
        if (signal?.aborted) {
          throw new Error('Export operation was aborted');
        }

        logger.log(`Rendering frame ${i + 1}/${timelineStates.length}`, 'AnimationRenderer');
        const frameBuffer = this.renderFrame(timelineStates[i]);
        logger.log(`Frame ${i + 1} buffer size: ${frameBuffer?.length || 0} bytes`, 'AnimationRenderer');
        
        const framePath = path.join(tempDir, `frame_${i.toString().padStart(5, '0')}.png`);
        logger.log(`Saving frame to: ${framePath}`, 'AnimationRenderer');
        fs.writeFileSync(framePath, frameBuffer);
        framePaths.push(framePath);
        logger.log(`Saved frame ${i + 1}`, 'AnimationRenderer');

        // Optionally dispatch progress event or allow cancellation between frames
        if (signal) {
          // Check if signal is actually an AbortSignal
          if (typeof signal === 'object' && signal !== null && 'addEventListener' in signal) {
            signal.addEventListener('abort', () => {
              // Clean up resources immediately on abort
              framePaths.forEach(framePath => {
                if (fs.existsSync(framePath)) {
                  fs.unlinkSync(framePath);
                }
              });
              if (fs.existsSync(tempDir)) {
                fs.rmdirSync(tempDir, { recursive: true });
              }
            }, { once: true });
          } else {
            console.warn('[AnimationRenderer] Invalid signal object provided:', signal);
          }
        }
      }

      // Use FFmpeg to combine frames into video
      return new Promise<void>((resolve, reject) => {
        // Check if operation has been aborted
        if (signal?.aborted) {
          // Clean up temporary files
          cleanupTempFiles();
          reject(new Error('Export operation was aborted'));
          return;
        }

        // Ensure output directory exists
        const outputDir = path.dirname(outputPath);
        if (!fs.existsSync(outputDir)) {
          fs.mkdirSync(outputDir, { recursive: true });
        }

        // Check if FFmpeg is available
        const ffmpegPath = process.env.FFMPEG_PATH || 'ffmpeg';
        
        logger.log( `ffmpegPath : ${process.env.FFMPEG_PATH}`, 'AnimationRenderer');
        // FFmpeg command to create video from frames
        const args = [
          '-y', // Overwrite output file
          '-r', this.frameRate.toString(), // Frame rate
          '-i', path.join(tempDir, 'frame_%05d.png'), // Input frames
          '-c:v', 'libx264', // Video codec
          '-pix_fmt', 'yuv420p', // Pixel format
          '-preset', 'medium', // Encoding preset
          '-crf', '23', // Constant rate factor (quality)
          outputPath // Output file
        ];

        logger.log(`  Starting FFmpeg with command:\n ${ffmpegPath} ${args.join(' ')}`, 'AnimationRenderer');
        
        const ffmpegProcess = spawn(ffmpegPath, args);
        
        let stdout = '';
        let stderr = '';
        
        ffmpegProcess.stdout.on('data', (data) => {
          stdout += data.toString();
        });
        
        ffmpegProcess.stderr.on('data', (data) => {
          stderr += data.toString();
        });
        
        // Handle abort signal during FFmpeg processing
        const handleAbort = () => {
          ffmpegProcess.kill('SIGINT');
          cleanupTempFiles();
          reject(new Error('Export operation was aborted'));
        };

        if (signal) {
          if (signal.aborted) {
            handleAbort();
            return;
          }
          
          signal.addEventListener('abort', handleAbort);
        }
        
        ffmpegProcess.on('close', (code) => {
          // Remove abort listener when process closes
          if (signal) {
            signal.removeEventListener('abort', handleAbort);
          }
          
          logger.log(`FFmpeg process exited with code ${code}`, 'AnimationRenderer');
          logger.log('FFmpeg stdout:', 'AnimationRenderer');
          logger.log(stdout, 'AnimationRenderer');
          logger.log('FFmpeg stderr:', 'AnimationRenderer');
          logger.log(stderr, 'AnimationRenderer');
          
          // Clean up temporary frames
          cleanupTempFiles();
          
          if (code === 0) {
            logger.log(`Video exported successfully to ${outputPath}`, 'AnimationRenderer');
            resolve();
          } else {
            const errorMessage = `FFmpeg process exited with code ${code}. Error: ${stderr}`;
            logger.error(errorMessage, 'AnimationRenderer');
            reject(new Error(errorMessage));
          }
        });
        
        ffmpegProcess.on('error', (error) => {
          // Remove abort listener on error
          if (signal) {
            signal.removeEventListener('abort', handleAbort);
          }
          
          logger.error(`Failed to start FFmpeg: ${error.message}`, 'AnimationRenderer');
          // Clean up on error
          cleanupTempFiles();
          reject(new Error(`Failed to start FFmpeg: ${error.message}`));
        });
        
        // Helper function to clean up temporary files
        function cleanupTempFiles() {
          try {
            framePaths.forEach(framePath => {
              if (fs.existsSync(framePath)) {
                fs.unlinkSync(framePath);
              }
            });
            
            if (fs.existsSync(tempDir)) {
              fs.rmdirSync(tempDir, { recursive: true });
            }
          } catch (cleanupError) {
            logger.warn('Error during cleanup:' + (cleanupError as Error).message, 'AnimationRenderer');
          }
        }
      });
    } catch (error) {
      logger.error('Error during video export: ' + (error as Error).message, 'AnimationRenderer');
      throw error;
    }
  }

  /**
   * Initializes the scene with provided data
   * @param sceneData - Scene data to initialize with
   */
  public setupScene(sceneData: SceneData): void {
    // Clear existing scene
    this.clearScene();
    
    const logger = Logger.getInstance();
    
    // Check if sceneData exists
    if (!sceneData) {
      logger.warn('No scene data provided, initializing empty scene', 'AnimationRenderer');
      return;
    }
    
    logger.log('Setting up scene with data', 'AnimationRenderer');
    logger.log(`Current project ID: ${this.currentProjectId}`, 'AnimationRenderer');
    
    // Based on elementData.type add elements to the scene manager
    // This feature implements real scene element instance creation logic:
    // 1. Iterate through scene element data
    // 2. Create corresponding instances based on element type
    // 3. Add elements to the scene manager
    if (sceneData.elements) {
      logger.log(`Creating ${sceneData.elements.length} elements`, 'AnimationRenderer');
      
      for (const elementData of sceneData.elements) {
        logger.log(`Processing element data: ${JSON.stringify(elementData)}`, 'AnimationRenderer');
        
        // Create scene element instance
        // This feature implements real scene element instance creation logic:
        // 1. Create corresponding instance based on element type
        // 2. Process element properties
        let element: SceneElement | null = null;
        
        // Extract properties from elementData
        const properties = elementData.properties || {};
        logger.log(`Element ${elementData.id as string} properties: ${JSON.stringify(properties)}`, 'AnimationRenderer');
        
        switch (elementData.type) {
          case 'character':
            logger.log(`Creating CharacterElement with id: ${elementData.id as string}`, 'AnimationRenderer');
            try {
              // Load skeleton data from file if skeletonFile is provided
              let skeletonData = properties.skeletonData || {};
              logger.log(`Initial skeletonData for ${elementData.id as string}: ${JSON.stringify(skeletonData)}`, 'AnimationRenderer');
              
              if (properties.skeletonFile && this.currentProjectId) {
                try {
                  const path = require('path');
                  const fs = require('fs');
                  
                  logger.log(`Loading skeleton file: ${properties.skeletonFile} for project: ${this.currentProjectId}`, 'AnimationRenderer');
                  // Construct the full path to the skeleton file
                  const workspacesDir = path.join(__dirname, '../../../workspaces');
                  let skeletonFilePath = '';
                  
                  // Handle different path formats
                  if (properties.skeletonFile.startsWith('assets/')) {
                    skeletonFilePath = path.join(workspacesDir, this.currentProjectId, properties.skeletonFile);
                  } else if (!properties.skeletonFile.includes('/') && !properties.skeletonFile.includes('\\')) {
                    // Just a filename, look in characters directory
                    skeletonFilePath = path.join(workspacesDir, this.currentProjectId, 'assets', 'characters', properties.skeletonFile);
                  } else {
                    skeletonFilePath = path.join(workspacesDir, this.currentProjectId, properties.skeletonFile);
                  }
                  
                  logger.log(`Full skeleton file path: ${skeletonFilePath}`, 'AnimationRenderer');
                  logger.log(`File exists: ${fs.existsSync(skeletonFilePath)}`, 'AnimationRenderer');
                  // Read and parse the skeleton file
                  if (fs.existsSync(skeletonFilePath)) {
                    const skeletonFileContent = fs.readFileSync(skeletonFilePath, 'utf8');
                    skeletonData = JSON.parse(skeletonFileContent);
                    logger.log(`Loaded skeleton data from ${skeletonFilePath}`, 'AnimationRenderer');
                    logger.log(`Skeleton data keys: ${Object.keys(skeletonData).join(', ')}`, 'AnimationRenderer');
                  } else {
                    logger.warn(`Skeleton file not found: ${skeletonFilePath}`, 'AnimationRenderer');
                    
                    // Try with .json extension if not already present
                    if (!skeletonFilePath.endsWith('.json')) {
                      const jsonSkeletonFilePath = skeletonFilePath + '.json';
                      logger.log(`Trying skeleton file with .json extension: ${jsonSkeletonFilePath}`, 'AnimationRenderer');
                      logger.log(`JSON file exists: ${fs.existsSync(jsonSkeletonFilePath)}`, 'AnimationRenderer');
                      if (fs.existsSync(jsonSkeletonFilePath)) {
                        const skeletonFileContent = fs.readFileSync(jsonSkeletonFilePath, 'utf8');
                        skeletonData = JSON.parse(skeletonFileContent);
                        logger.log(`Loaded skeleton data from ${jsonSkeletonFilePath}`, 'AnimationRenderer');
                        logger.log(`Skeleton data keys: ${Object.keys(skeletonData).join(', ')}`, 'AnimationRenderer');
                      }
                    }
                  }
                } catch (error) {
                  logger.error(`Error loading skeleton file for ${elementData.id as string}: ${(error as Error).message}`, 'AnimationRenderer');
                }
              } else {
                logger.log(`No skeletonFile provided or no currentProjectId. skeletonFile: ${properties.skeletonFile as string}, currentProjectId: ${this.currentProjectId as string}`, 'AnimationRenderer');
              }
              
              element = new CharacterElement(
                elementData.id,
                skeletonData,
                properties.position,
                properties.width || 200,  // Provide default width
                properties.height || 300, // Provide default height
                properties.scale,
                properties.rotation,
                properties.visible,
                properties.zIndex
              );
              logger.log(`Successfully created CharacterElement with id: ${elementData.id as string}`, 'AnimationRenderer');
            } catch (error) {
              logger.error(`Error creating CharacterElement with id: ${elementData.id as string}: ${(error as Error).message}`, 'AnimationRenderer');
            }
            break;
          case 'background':
            logger.log(`Creating BackgroundElement with id: ${elementData.id as string}`, 'AnimationRenderer');
            try {
              element = new BackgroundElement(
                elementData.id,
                properties.imageUrl || properties.backgroundId || '',
                properties.position,
                properties.width,
                properties.height,
                properties.scale,
                properties.rotation,
                properties.visible,
                properties.zIndex,
                this.currentProjectId // Pass project ID to background element
              );
              logger.log(`Successfully created BackgroundElement with id: ${elementData.id as string}`, 'AnimationRenderer');
            } catch (error) {
              logger.error(`Error creating BackgroundElement with id: ${elementData.id as string}: ${(error as Error).message}`, 'AnimationRenderer');
            }
            break;
          case 'ui':
            logger.log(`Creating UIElement with id: ${elementData.id as string}`, 'AnimationRenderer');
            try {
              element = new UIElement(
                elementData.id,
                properties.text || '',
                properties.position,
                properties.font,
                properties.fontSize,
                properties.textColor,
                properties.backgroundColor,
                properties.width,
                properties.height,
                properties.scale,
                properties.rotation,
                properties.visible,
                properties.zIndex
              );
              logger.log(`Successfully created UIElement with id: ${elementData.id as string}`, 'AnimationRenderer');
            } catch (error) {
              logger.error(`Error creating UIElement with id: ${elementData.id as string}: ${(error as Error).message}`, 'AnimationRenderer');
            }
            break;
          default:
            logger.warn(`Unknown element type: ${elementData.type}`, 'AnimationRenderer');
            continue;
        }
        
        // In actual implementation, you might also need to:
        // 1. Validate element data
        // 2. Handle element dependencies
        // 3. Initialize element state
        // 4. Set up element animation controller
        
        if (element) {
          logger.log(`Adding element ${elementData.id as string} to scene manager`, 'AnimationRenderer');
          this.sceneManager.addElement(element);
          logger.log(`Element ${elementData.id as string} added successfully`, 'AnimationRenderer');
        } else {
          logger.warn(`Element ${elementData.id as string} was not created successfully`, 'AnimationRenderer');
        }
      }
      
      const elements = this.sceneManager.getAllElements();
      logger.log(`Created ${elements.length} elements:`, 'AnimationRenderer');
      elements.forEach((element, index) => {
        logger.log(`  ${index + 1}. ${element.getId()} (${element.constructor.name})`, 'AnimationRenderer');
      });
    } else {
      logger.warn('No elements found in sceneData', 'AnimationRenderer');
    }
  }

  /**
   * Clears the current scene
   */
  public clearScene(): void {
    this.sceneManager.clearAllElements();
    this.canvasManager.clear();
  }

  /**
   * Renders all elements in the scene
   */
  private renderElements(): void {
    const logger = Logger.getInstance();
    const context = this.canvasManager.getContext();

    if (!context) {
      logger.error('Canvas context is not available', 'AnimationRenderer');
      return;
    }

    // Apply camera transformations
    this.camera.transformContext(context);

    // Get all elements and sort by z-index
    const elements = this.sceneManager.getAllElements();
    logger.log(`Found ${elements.length} elements to render`, 'AnimationRenderer');
    
    // Log all elements before sorting
    elements.forEach((element, index) => {
      logger.log(`Element ${index}: id=${element.getId()}, type=${element.constructor.name}, visible=${element.isVisible()}, zIndex=${element.getZIndex()}`, 'AnimationRenderer');
    });
    
    elements.sort((a, b) => a.getZIndex() - b.getZIndex());
    
    // Log elements after sorting
    logger.log('Elements sorted by z-index:', 'AnimationRenderer');
    elements.forEach((element, index) => {
      logger.log(`Sorted Element ${index}: id=${element.getId()}, type=${element.constructor.name}, visible=${element.isVisible()}, zIndex=${element.getZIndex()}`, 'AnimationRenderer');
    });

    // Render each element
    for (const element of elements) {
      logger.log(`Rendering element: ${element.getId()} (${element.constructor.name}), visible: ${element.isVisible()}`, 'AnimationRenderer');
      if (element.isVisible()) {
        logger.log(`Calling render on element: ${element.getId()}`, 'AnimationRenderer');
        element.render(context);
        logger.log(`Finished rendering element: ${element.getId()}`, 'AnimationRenderer');
      } else {
        logger.log(`Skipping rendering of element: ${element.getId()} because it is not visible`, 'AnimationRenderer');
      }
    }
    
    logger.log(`Completed rendering ${elements.length} elements`, 'AnimationRenderer');
  }

  /**
   * Gets the scene manager instance
   * @returns SceneManager - The scene manager
   */
  public getSceneManager(): SceneManager {
    return this.sceneManager;
  }

  /**
   * Gets the camera instance
   * @returns Camera - The camera
   */
  public getCamera(): Camera {
    return this.camera;
  }

  /**
   * Gets the canvas manager instance
   * @returns CanvasManager - The canvas manager
   */
  public getCanvasManager(): CanvasManager {
    return this.canvasManager;
  }

  /**
   * Resizes the renderer
   * @param width - New width
   * @param height - New height
   */
  public resize(width: number, height: number): void {
    this.width = width;
    this.height = height;
    this.canvasManager.resize(width, height);
  }
}